Skip to content

feat(gh-copilot): add AI credit usage billing metrics and fix report-body double-read - #9019

Merged
klesh merged 4 commits into
apache:mainfrom
PankajChaudhari-Atos:feat-gh-copilot-ai-credit-and-report-fix
Jul 30, 2026
Merged

feat(gh-copilot): add AI credit usage billing metrics and fix report-body double-read#9019
klesh merged 4 commits into
apache:mainfrom
PankajChaudhari-Atos:feat-gh-copilot-ai-credit-and-report-fix

Conversation

@PankajChaudhari-Atos

Copy link
Copy Markdown

Summary

This PR bundles two related gh-copilot changes (18 files):

  1. feat: AI credit usage billing metrics (ports feat(gh-copilot): add AI credit usage billing metrics collection #8980)
    Adds collection/extraction of GitHub Copilot AI credit usage billing
    metrics at org, user, and enterprise levels — new models, tasks
    (ai_credit_collector/ai_credit_extractor), a migration for the new
    billing tables, and e2e snapshot fixtures.

  2. fix: report-body double-read zeroing Copilot user/enterprise metrics
    parseUserMetricsReportResponse and parseRawReportResponse read the
    HTTP response body once (via io.ReadAll) and then called
    parseReportMetadataResponse(res, ...), which re-read the already-consumed
    body. The second read returned empty → "Report metadata response was empty,
    skipping" → 0 records, silently zeroing user and enterprise Copilot metrics
    (org metrics were unaffected because that path reads the body once inline).
    Fix: parse the already-read body via parseReportMetadata(body, logger)
    and drop the redundant re-read.

Testing

  • go build ./plugins/gh-copilot/..., go vet, and go test ./plugins/gh-copilot/models/... pass.
  • Behavioral validation on a live deployment: before the fix, user-metrics
    collection logged "empty, skipping" and produced 0 rows; after the fix, it
    downloads report files and populates _raw_copilot_user_metrics and
    _tool_copilot_user_daily_metrics.

Notes

PankajChaudhari-Atos and others added 4 commits July 29, 2026 11:11
Adds collection and extraction of GitHub Copilot AI credit usage
billing metrics at org, user, and enterprise levels, including new
models, tasks (ai_credit_collector/extractor), a migration for the
new billing tables, and e2e snapshot fixtures.

Ports the work from apache#8980.

Co-authored-by: Andrei Savu <54935810+AndreiS-gh@users.noreply.github.com>
Signed-off-by: Pankaj Chaudhari <pankaj.chaudhari@atos.net>
The user and enterprise Copilot metrics collectors read the report
metadata HTTP response body with io.ReadAll, then called
parseReportMetadataResponse(res, ...) which read res.Body a second time.
Since the body was already consumed, the second read returned empty,
parseReportMetadata logged "Report metadata response was empty,
skipping" and returned nil, so the collectors produced zero records.

Parse the metadata from the body already read instead. The organization
collector was unaffected because it reads the body only once inline.

Fixes empty _raw_copilot_user_metrics and _raw_copilot_enterprise_metrics
(and downstream _tool_copilot_user_daily_metrics) while org metrics work.

Signed-off-by: Pankaj Chaudhari <pankaj.chaudhari@atos.net>
- migrationscripts: inline snapshot structs instead of importing models pkg
- user_metrics: fix gofmt struct-tag alignment
- ai_credit_collector/extractor: fix ASF license header (capital The)
- report_download_helper: remove unused readReportMetadataBody/parseReportMetadataResponse
- metrics_collector_test: retarget tests to parseReportMetadata/ignoreNoContent

@klesh klesh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.
Thanks for your contribution.

@klesh
klesh merged commit 705280d into apache:main Jul 30, 2026
10 checks passed
@PankajChaudhari-Atos
PankajChaudhari-Atos deleted the feat-gh-copilot-ai-credit-and-report-fix branch July 30, 2026 16:30
DoDiODev added a commit to DoDiODev/devlake that referenced this pull request Aug 3, 2026
The cross-plugin schema-drift guard added by this PR caught a fourth
occurrence of the same bug class, introduced by apache#9019:

  _tool_copilot_enterprise_ai_credit_usage
  _tool_copilot_org_ai_credit_usage
  _tool_copilot_user_ai_credit_usage

all lack gross_quantity, discount_quantity, net_quantity, price_per_unit,
gross_amount, discount_amount and net_amount, while the runtime models
models.GhCopilot{Enterprise,Org,User}AiCreditUsage declare them. Writing a
record therefore fails with "Unknown column 'gross_quantity' in 'field list'".

Root cause: 20260708_add_ai_credit_usage_metrics.go declares those seven
columns through an anonymous embedded struct whose TYPE NAME IS UNEXPORTED

    creditUsageBreakdown20260708 `gorm:"embedded"`

and GORM's schema parser skips anonymous fields of unexported types, so
AutoMigrate never created the columns.

Add a new, additive migration that re-runs AutoMigrateTables on structs which
embed an EXPORTED type, so the columns are actually picked up. AutoMigrate only
adds absent columns, so this is safe on populated tables. The original script is
left untouched: migration scripts are append-only and its version is already
recorded in _devlake_migration_history.

Verified with the cross-plugin guard on MySQL 8.4.10 and PostgreSQL 17.2:
44/44 plugins pass (was 43/44 with gh-copilot failing).

Signed-off-by: DoDiODev <DoDiDev@proton.me>
DoDiODev added a commit to DoDiODev/devlake that referenced this pull request Aug 3, 2026
The cross-plugin schema-drift guard added by this PR caught a fourth
occurrence of the same bug class, introduced by apache#9019:

  _tool_copilot_enterprise_ai_credit_usage
  _tool_copilot_org_ai_credit_usage
  _tool_copilot_user_ai_credit_usage

all lack gross_quantity, discount_quantity, net_quantity, price_per_unit,
gross_amount, discount_amount and net_amount, while the runtime models
models.GhCopilot{Enterprise,Org,User}AiCreditUsage declare them inline. Writing
a record therefore fails with "Unknown column 'gross_quantity' in 'field list'".

Root cause: 20260708_add_ai_credit_usage_metrics.go declares those seven columns
through an anonymous embedded struct whose TYPE NAME IS UNEXPORTED

    creditUsageBreakdown20260708 `gorm:"embedded"`

and GORM's schema parser skips anonymous fields of unexported types, so
AutoMigrate never created the columns.

Add a new, additive migration that AutoMigrates the missing columns. It only
adds absent columns, so it is a no-op on databases that already have them and
safe on populated tables. The original script is left untouched: migration
scripts are append-only and its version is already recorded in
_devlake_migration_history.

Verified with the cross-plugin guard against a fresh database on MySQL 8.4.10
and PostgreSQL 17.2: 44/44 plugins pass (was 43/44 with gh-copilot failing on
21 missing columns).

Signed-off-by: DoDiODev <DoDiDev@proton.me>
klesh pushed a commit that referenced this pull request Aug 3, 2026
…ts (#9015)

* fix(jira): add missing _raw_data_* columns to _tool_jira_sprint_reports

The Sprint Report migration 20260722_add_sprint_report_table.go creates
_tool_jira_sprint_reports from a struct that does not embed
common.NoPKModel, while the runtime model models.JiraSprintReport does.
The columns _raw_data_params / _raw_data_table / _raw_data_id /
_raw_data_remark (plus created_at, updated_at) were therefore never
created, so the ApiExtractor cleanup query

    WHERE _raw_data_table = ? AND _raw_data_params = ?

made the extractSprintReport subtask fail with
"Error 1054 (42S22): Unknown column '_raw_data_table' in 'where clause'".

Add a new, additive migration that re-runs AutoMigrateTables on a struct
embedding archived.NoPKModel. The original migration is left untouched:
migration scripts are append-only, and editing it would not repair
databases that already recorded its version.

Add two schema-drift regression guards that run the REAL migration
scripts instead of AutoMigrate-ing the runtime model, which would hide
this class of drift:

  * plugins/jira/e2e/migration_schema_test.go - Jira-specific guard.
  * plugins/schema_e2e/migration_schema_test.go - cross-plugin guard for
    every built-in Go plugin, including TestAllGoPluginsListed so the
    guard stays complete when a new plugin is added.

Both guards run the migrations against a dedicated, empty database
created by the new helper e2ehelper.NewIsolatedMigrationDb: the shared
e2e database is polluted by the other e2e tests, which AutoMigrate
tables without recording anything in _devlake_migration_history, so
running the real scripts against it fails with errors such as
"Table 'cicd_pipeline_commits' already exists".

The cross-plugin guard immediately uncovered three pre-existing drifts
of the same class, each fixed with its own additive migration:

  * _tool_taiga_scope_configs      - missing type_mappings
  * _tool_teambition_scope_configs - missing id, created_at, updated_at
  * _tool_testmo_scope_configs     - missing connection_id, name

The teambition table has no primary key at all, and its missing `id` is
an auto-increment primary key, which AutoMigrate cannot append to an
existing table (MySQL: "Incorrect table definition; there can be only
one auto column and it must be defined as a key"). That column is
therefore added with explicit DDL, which also keeps the ids of existing
rows and the sequence/counter in sync on both MySQL and PostgreSQL.

Finally, exclude plugins/schema_e2e from scripts/build-plugins.sh: it is
not a plugin and has no main package, which broke `make build-plugin`
with "-buildmode=plugin requires exactly one main package".

Signed-off-by: DoDiODev <DoDiDev@proton.me>

* fix(gh-copilot): add missing AI credit usage breakdown columns

The cross-plugin schema-drift guard added by this PR caught a fourth
occurrence of the same bug class, introduced by #9019:

  _tool_copilot_enterprise_ai_credit_usage
  _tool_copilot_org_ai_credit_usage
  _tool_copilot_user_ai_credit_usage

all lack gross_quantity, discount_quantity, net_quantity, price_per_unit,
gross_amount, discount_amount and net_amount, while the runtime models
models.GhCopilot{Enterprise,Org,User}AiCreditUsage declare them inline. Writing
a record therefore fails with "Unknown column 'gross_quantity' in 'field list'".

Root cause: 20260708_add_ai_credit_usage_metrics.go declares those seven columns
through an anonymous embedded struct whose TYPE NAME IS UNEXPORTED

    creditUsageBreakdown20260708 `gorm:"embedded"`

and GORM's schema parser skips anonymous fields of unexported types, so
AutoMigrate never created the columns.

Add a new, additive migration that AutoMigrates the missing columns. It only
adds absent columns, so it is a no-op on databases that already have them and
safe on populated tables. The original script is left untouched: migration
scripts are append-only and its version is already recorded in
_devlake_migration_history.

Verified with the cross-plugin guard against a fresh database on MySQL 8.4.10
and PostgreSQL 17.2: 44/44 plugins pass (was 43/44 with gh-copilot failing on
21 missing columns).

Signed-off-by: DoDiODev <DoDiDev@proton.me>

* test(schema): guard the migration upgrade path on populated tables

Review feedback on #9015: TestMigrationSchemaMatchesModels proves the END STATE
of a fresh migration run matches the runtime models, but every table it inspects
is empty, so it never exercises the upgrade path of a repair migration on a
database that already holds rows -- which is the only situation those migrations
exist for.

Add TestMigrationUpgradePathOnPopulatedTables, which for every repair migration
in this PR

  1. recreates the table exactly as the buggy migration left it,
  2. inserts rows,
  3. runs ONLY that repair script,
  4. asserts the columns were added, the rows survived, the table has a primary
     key and auto-increment ids were backfilled (plus that a subsequent INSERT
     still works, i.e. the sequence/counter is in sync).

Step 4 covers what a column-presence check cannot see. Negative test, with the
explicit AUTO_INCREMENT DDL in the teambition script replaced by a plain
AutoMigrate:

  MySQL      -> FAIL, migration errors out (Error 1075)
  PostgreSQL -> FAIL, "table has no primary key after ..." (AutoMigrate happily
                adds `bigserial` without a key, so this is invisible to the
                column-only guard)

Covered: _tool_jira_sprint_reports, _tool_taiga_scope_configs,
_tool_teambition_scope_configs, _tool_testmo_scope_configs and the three
_tool_copilot_*_ai_credit_usage tables.

The scripts are looked up through each plugin's own MigrationScripts() by
version, so the test fails if one is removed or renumbered.

Verified on MySQL 8.4.10 and PostgreSQL 17.2: 51/51 subtests pass
(44 plugins + 7 upgrade-path cases).

Signed-off-by: DoDiODev <DoDiDev@proton.me>

---------

Signed-off-by: DoDiODev <DoDiDev@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants